/*
 * Copyright (C) 2020 Beijing Yishu Technology Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.miqt.plugin.inject.compiler;

import com.google.auto.service.AutoService;
import com.miqt.plugin.inject.annotation.After;
import com.miqt.plugin.inject.annotation.AfterSuper;
import com.miqt.plugin.inject.annotation.AfterSupers;
import com.miqt.plugin.inject.annotation.Afters;
import com.miqt.plugin.inject.annotation.Before;
import com.miqt.plugin.inject.annotation.BeforeSuper;
import com.miqt.plugin.inject.annotation.BeforeSupers;
import com.miqt.plugin.inject.annotation.Befores;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Types;

import org.objectweb.asm.Type;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

@AutoService(Processor.class)
public class InjectProcessor extends AbstractProcessor {
    private static final String TAG = "InjectProcessor";
    private static final boolean DEBUG = false;
    private static final String LICENSE_HEADER = "/*\n" +
            " * Copyright (C) 2020 Beijing Yishu Technology Co., Ltd.\n" +
            " *\n" +
            " * Licensed under the Apache License, Version 2.0 (the \"License\");\n" +
            " * you may not use this file except in compliance with the License.\n" +
            " * You may obtain a copy of the License at\n" +
            " *\n" +
            " *      http://www.apache.org/licenses/LICENSE-2.0\n" +
            " *\n" +
            " * Unless required by applicable law or agreed to in writing, software\n" +
            " * distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
            " * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
            " * See the License for the specific language governing permissions and\n" +
            " * limitations under the License.\n" +
            " */";

    private final List<List<Object>> mAroundHookClassesArgs = new ArrayList<>();
    private final List<List<Object>> mSuperHookClassesArgs = new ArrayList<>();

    private Messager mMessager;
    private ProcessingEnvironment processingEnv;
    private String testPath;

    public InjectProcessor(String testPath) {
        this.testPath = testPath;
    }

    public InjectProcessor() {
        this.testPath = null;
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
        this.processingEnv = processingEnvironment;
        log("InjectProcessor init finish");
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add(Before.class.getCanonicalName());
        types.add(After.class.getCanonicalName());
        types.add(BeforeSuper.class.getCanonicalName());
        types.add(AfterSuper.class.getCanonicalName());

        types.add(Befores.class.getCanonicalName());
        types.add(Afters.class.getCanonicalName());
        types.add(BeforeSupers.class.getCanonicalName());
        types.add(AfterSupers.class.getCanonicalName());
        return types;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_8;
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Class<? extends Annotation>[] allAnnotations = new Class[]{Before.class, After.class, BeforeSuper.class, AfterSuper.class,
                Befores.class, Afters.class, BeforeSupers.class, AfterSupers.class};
        for (Class<? extends Annotation> annotation : allAnnotations) {
            analyzeAnnotation(annotation, roundEnvironment);
        }

        generateJavaFile();
        return false;
    }

    private void analyzeAnnotation(Class<? extends Annotation> clazz, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(clazz);
        // 获取实现指定注解的类的包装
        for (Element element : elements) {
            log("================[@" + clazz.getSimpleName() + "] " + element.getEnclosingElement() + "#" + element.getSimpleName() + "================");
            if (clazz == Befores.class || clazz == Afters.class || clazz == BeforeSupers.class || clazz == AfterSupers.class) {
                // 找到注解的包装类 , 需要去拿里边的值
                AnnotationMirror mirror = com.miqt.plugin.inject.compiler.AnnotationUtil.findAnnotationMirror(element, clazz);
                if (mirror == null) {
                    continue;
                }
                // 获取 Befores 和 Afters BeforeSupers AfterSupers 中的注解的包装类
                List<AnnotationMirror> annotations = com.miqt.plugin.inject.compiler.AnnotationUtil.getAnnotations(mirror, "value");
                if (annotations != null && !annotations.isEmpty()) {
                    for (AnnotationMirror annotation : annotations) {
                        stashHookClassesArgs(element, annotation);
                    }
                }
            } else {
                stashHookClassesArgs(element, com.miqt.plugin.inject.compiler.AnnotationUtil.findAnnotationMirror(element, clazz));
            }
        }
    }

    /**
     * public @interface Before {
     *     Class<?> clazz();
     *
     *     String method();
     *
     *     Class<?>[] parameterTypes() default {};
     *
     *     Class<?> returnType() default void.class;
     * }
     */
    private void stashHookClassesArgs(Element element, AnnotationMirror annotationMirror) {
        // 实体 和 指定的注解的值
        // 获取注解中 class的值
        String originalTargetClassName = com.miqt.plugin.inject.compiler.AnnotationUtil.getClassValue(processingEnv, annotationMirror, "clazz");
        // 根据class 去得到指定的名称
        String targetClassName = TypeUtil.getInternalName(originalTargetClassName);
        // 获取指定的方法名称
        String targetMethodName = com.miqt.plugin.inject.compiler.AnnotationUtil.getStringValue(annotationMirror, "method");
        // 方法参数类型
        List<String> targetParameters = com.miqt.plugin.inject.compiler.AnnotationUtil.getClassesValue(annotationMirror, "parameterTypes");
        if (targetParameters == null) {
            targetParameters = new ArrayList<>();
        }
        Type[] argumentTypes = new Type[targetParameters.size()];
        for (int i = 0; i < targetParameters.size(); i++) {
            argumentTypes[i] = TypeUtil.getType(targetParameters.get(i));
        }
        // 获取返回的参数
        String targetReturnType = com.miqt.plugin.inject.compiler.AnnotationUtil.getClassValue(processingEnv, annotationMirror, "returnType");
        if (targetReturnType == null) {
            targetReturnType = void.class.getName();
        }
        Type returnType = TypeUtil.getType(targetReturnType);
        // 根据入参和出参 获取方法的修饰
        String targetDesc = Type.getMethodDescriptor(returnType, argumentTypes);

        String originalInjectClass = element.getEnclosingElement().toString();
        String injectClass = TypeUtil.getInternalName(originalInjectClass);
        String injectMethod = element.getSimpleName().toString();

//        if (element instanceof ExecutableElement) {
//            List<String> injectParameters = new ArrayList<>();
//            List<? extends VariableElement> methodParams = ((ExecutableElement) element).getParameters();
//            for (VariableElement param : methodParams) {
//                String className = param.asType().toString();
//                String convertName = AnnotationUtil.getConvertName(processingEnv, className);
//                injectParameters.add(convertName);
//            }
//            System.out.println("[change]:" + injectParameters.toString());
//        }

        if (element instanceof Symbol.MethodSymbol) {
            List<String> injectParameters = new ArrayList<>();
            Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) element;
            List<Symbol.VarSymbol> parameters = methodSymbol.getParameters();
            for (Symbol.VarSymbol parameter : parameters) {
                injectParameters.add(parameter.asType().accept(new Types.DefaultTypeVisitor<String, Void>() {
                    @Override
                    public String visitArrayType(com.sun.tools.javac.code.Type.ArrayType t, Void unused) {
                        return t.elemtype.accept(this, null) + "[]";
                    }

                    @Override
                    public String visitType(com.sun.tools.javac.code.Type type, Void unused) {
                        return type.asElement().flatName().toString();
                    }
                }, null));
            }
            argumentTypes = new Type[injectParameters.size()];
            for (int i = 0; i < injectParameters.size(); i++) {
                argumentTypes[i] = TypeUtil.getType(injectParameters.get(i));
            }
            String injectReturnType = void.class.getName();
            returnType = TypeUtil.getType(injectReturnType);
            String injectDesc = Type.getMethodDescriptor(returnType, argumentTypes);

            String annotationClass = annotationMirror.getAnnotationType().toString();
            boolean isAfter = After.class.getName().equals(annotationClass) || AfterSuper.class.getName().equals(annotationClass);

            log(originalTargetClassName + "#" + targetMethodName);
            if (Before.class.getName().equals(annotationClass) || After.class.getName().equals(annotationClass)) {
                mAroundHookClassesArgs.add(Arrays.<Object>asList(targetClassName, targetMethodName, targetDesc, injectClass, injectMethod, injectDesc, isAfter));
            } else {
                mSuperHookClassesArgs.add(Arrays.<Object>asList(targetClassName, targetMethodName, targetDesc, injectClass, injectMethod, injectDesc, isAfter));
            }
        }
    }


    private void log(String msg) {
        if (DEBUG) mMessager.printMessage(Diagnostic.Kind.NOTE, TAG + ": " + msg);
    }

    private void generateJavaFile() {
        try {
            TypeSpec.Builder builder = TypeSpec.classBuilder("HookTargets")
                    .addModifiers(Modifier.PUBLIC);
            ClassName listClass = ClassName.get(ArrayList.class);
            ClassName collectionsClass = ClassName.get(Collections.class);
            builder.addJavadoc("该Class是由 inject-compiler 自动生成的，请不要随意更改！");

            builder.addField(FieldSpec.builder(listClass, "AROUND_HOOK_TARGETS")
                    .addModifiers(Modifier.PRIVATE)
                    .addModifiers(Modifier.STATIC)
                    .addModifiers(Modifier.FINAL)
                    .initializer("new $T<>()", listClass)
                    .build());
            builder.addField(FieldSpec.builder(listClass, "SUPER_HOOK_TARGETS")
                    .addModifiers(Modifier.PRIVATE)
                    .addModifiers(Modifier.STATIC)
                    .addModifiers(Modifier.FINAL)
                    .initializer("new $T<>()", listClass)
                    .build());

            CodeBlock.Builder staticBlock = CodeBlock.builder();
            for (List<Object> arg : mAroundHookClassesArgs) {
                staticBlock.addStatement("AROUND_HOOK_TARGETS.add(new Object[]{$S, $S, $S, $S, $S, $S, $L})",
                        arg.get(0), arg.get(1), arg.get(2), arg.get(3), arg.get(4), arg.get(5), arg.get(6));
            }
            for (List<Object> arg : mSuperHookClassesArgs) {
                staticBlock.addStatement("SUPER_HOOK_TARGETS.add(new Object[]{$S, $S, $S, $S, $S, $S, $L})",
                        arg.get(0), arg.get(1), arg.get(2), arg.get(3), arg.get(4), arg.get(5), arg.get(6));
            }
            builder.addStaticBlock(staticBlock.build());

            MethodSpec getAroundHookTargets = MethodSpec.methodBuilder("getAroundHookTargets")
                    .addModifiers(Modifier.FINAL)
                    .addModifiers(Modifier.PUBLIC)
                    .addModifiers(Modifier.STATIC)
                    .returns(List.class)
                    .addCode("return $T.unmodifiableList(AROUND_HOOK_TARGETS);", collectionsClass).build();
            builder.addMethod(getAroundHookTargets);
            MethodSpec getSuperHookTargets = MethodSpec.methodBuilder("getSuperHookTargets")
                    .addModifiers(Modifier.FINAL)
                    .addModifiers(Modifier.PUBLIC)
                    .addModifiers(Modifier.STATIC)
                    .returns(List.class)
                    .addCode("return $T.unmodifiableList(SUPER_HOOK_TARGETS);", collectionsClass).build();
            builder.addMethod(getSuperHookTargets);

            JavaFile javaFile = JavaFile.builder("com.miqt.plugin.inject", builder.build())
                    .skipJavaLangImports(true)
                    .build();
            generatePerfectJava(javaFile);

        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    private void generatePerfectJava(JavaFile javaFile) throws IOException {
        javaFile.writeTo(processingEnv.getFiler());
//        String fileName = javaFile.packageName.isEmpty()
//                ? javaFile.typeSpec.name
//                : javaFile.packageName + "." + javaFile.typeSpec.name;
//        List<Element> originatingElements = javaFile.typeSpec.originatingElements;
//        JavaFileObject filerSourceFile = processingEnv.getFiler().createSourceFile(fileName,
//                originatingElements.toArray(new Element[originatingElements.size()]));
//
//        StringBuilder builder = new StringBuilder(LICENSE_HEADER);
//        builder.append(System.lineSeparator())
//                .append(javaFile.toString());
//        try (Writer writer = filerSourceFile.openWriter()) {
//            writer.write(builder.toString());
//        } catch (Exception e) {
//            try {
//                filerSourceFile.delete();
//            } catch (Exception ignored) {
//            }
//            throw e;
//        }
    }

    private String getProjectRootPath() throws UnsupportedEncodingException {
        File userDir = new File(System.getProperty("user.dir"));
        File findPlugin = getGrowingPluginPath(userDir, 0);
        if (findPlugin != null) {
            return URLDecoder.decode(findPlugin.getPath(), StandardCharsets.UTF_8.name());
        }
        return System.getProperty("user.dir") + "/growingio-autotracker-gradle-plugin";
    }

    private File getGrowingPluginPath(File userDir, int depth) {
        if (depth >= 5 || userDir == null || !userDir.isDirectory()) return null;
        File[] dirList = userDir.listFiles();
        if (dirList == null || dirList.length == 0)
            return getGrowingPluginPath(userDir.getParentFile(), depth + 1);
        for (File file : dirList) {
            if (file.isDirectory() && file.getName().equals("growingio-autotracker-gradle-plugin")) {
                return file;
            }
        }
        return getGrowingPluginPath(userDir.getParentFile(), depth + 1);
    }


}